ARouter 使用

ARouter 简介

原生路由方式的缺点

  • 显式 intent 的方式会存在直接的类依赖问题,耦合严重,隐式 intent 的方式会出现规则集中式管理,协作困难,配置规则都在 Manifest 中,扩展性较差。

  • 跳转过程无法控制,跳转失败的情况下,无法降级,直接抛出运营级异常。

ARouter的优势

APP 组件化开发时,自定义路由框架可以非常好地解决整个 APP 完成组件化之后模块之间没有耦合的问题,在原生和 H5 混合开发时,能够提供非常便捷和统一的跳转方式。

ARouter 的优点如下:

  • 可以直接解析 URL 路由,解析参数并赋值到对应目标字段的页面中。

  • 支持多模块项目,提供 IOC 容器,通过注解方式处理。

  • 允许自定义拦截器,解决一些面向行为编程上出现的问题。

  • 映射关系自动注册,灵活的降级策略。

ARouter 的使用

初始化

按照官方文档进行库的依赖即可,这里说一下 API 的使用,至于为什么这样用,源码分析的时候再讲。首先要进行 ARouter 的初始化,官方推荐在 Application 中进行,如下:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        ARouter.openLog();// 打印日志
        // 开启调试模式(如果在InstantRun(就是AndroidStudio2.0以后新增的一个可以减少很多编译时间的运行机制)模式下
        运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
        ARouter.openDebug();
        ARouter.init(this);
    }
}

注意:在线上环境记得要把日志输出和 debug 模式关闭,在 debug 模式下会有一些逻辑上的额外处理。

添加注解

在需要进行路由的 ActivityFragment 的页面添加注解,如下:

// 在支持路由的页面上添加注解(必须写)
// 这里的路径需要注意的是至少需要有两级,/xx/xx,名字你自己起
@Route(path = "/test/Test1Activity")
public class Test1Activity extends BaseActivity {

}

这里用到了 Route 注解类,该类用于标注需要路由的目标页面,path 方法用来标明路由地址。

注意:路由地址必须至少有两级,第一级为默认的分组。

路由分发

分发的方式有很多种,可通过路由地址进行跳转,如下:

// activity 可传可不传,能传最好传
ARouter.getInstance().build("/test/Test1Activity").navigation(activity);

可通过 Uri 进行跳转,如下:

Uri uri = Uri.parse("http://www.wmzx.com/test/Test1Activity");
ARouter.getInstance().build(uri).navigation(activity);

带回调结果的跳转(startActivityForResult),如下:

// 利用重载方法,传入请求码即可
ARouter.getInstance().build("/test/Test2Activity").navigation(activity,666);

上面这些都是简单的跳转方式,下面看一下带参数的跳转怎么调用:

TestParcelable testParcelable = new TestParcelable("fanda", "man");
TestObj testObj = new TestObj("liuhang", "woman");
List<TestObj> testObjList = new ArrayList<>();
testObjList.add(testObj);

Map<String, List<TestObj>> map = new HashMap<>();
map.put("testMap", testObjList);

//Activity实例是拿不到的,不能这样处理,test3Activity为null
//仅仅这样传递自定义对象会报错,需要对象解析服务来处理 SerializationService
Uri uri = Uri.parse("pitaya://www.wmzx.com/test/Test3Activity");
Test3Activity test3Activity = (Test3Activity) ARouter.getInstance().build(uri)
        .withString("teacherName", "fana")
        .withInt("age", 18)
        .withParcelable("testPac", testParcelable)
        .withObject("testObj", testObj)
        .withObject("testObjList", testObjList)
        .withObject("testMap", map)
        .navigation(this, new NavigationCallbackImpl());//有回调的时候,会优先于全局降级,可以单独对跳转做处理
// 如果设置了NavigationCallbackImpl ,即要使用单独降级处理,不会再触发 DegradeService

通过各种 .withXXX 的方法传入各种类型的参数,key 为需要解析的字段名,value 为对应的值。

注意:传递自定义的对象时,需要用到对象解析服务 SerializationService 来处理,这是一个接口,具体的解析逻辑由开发者自己处理,开发者需要实现该接口并用 Route 注解标明路由,在需要用到该对象时,底层会通过反射构建该实例进行解析处理。

Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
    @Override
    public void init(Context context) {
        Log.e("fanda", "JsonServiceImpl init !");
    }

    @Override
    public <T> T json2Object(String input, Class<T> clazz) {
        return JSON.parseObject(input,clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return JSON.toJSONString(instance);
    }

    @Override
    public <T> T parseObject(String input, Type clazz) {
        return JSON.parseObject(input,clazz);
    }
}

其实 ARouter 在底层的数据传递上用的还是 Bundle ,上面传递的各种数据,最终都会添加到 Bundle 对象上,最后会构建 Intent 对象并添加上该 Bundle 来进行数据传输,所以在指定路由界面上,可以通过 getIntent 的方式进行数据的获取。实际上,ARouter 提供了一种更加便捷的方式来注入数据,那就是通过注解的方式,我们只需要在需要注入数据的对象上面添加 Autowired 注解,然后在对应的路由界面上添加如下代码即可:

//依赖注入参数或服务,如果没有,参数需要手动 getIntent 获取
ARouter.getInstance().inject(this);

这行代码会通过 APT 生成的注入辅助类来对参数进行自动赋值操作,避免重复的手动赋值操作,示例代码如下:

@Route(path = "/test/Test3Activity")
public class Test3Activity extends BaseActivity {

    @Autowired(name = "teacherName")  //别名,如果设置了 name 这个属性,则传参时需要用别名
    protected String name;  //不能设置为 private

    @Autowired  //需要添加该注解才会自动注入
    public int age;

    @Autowired
    public TestParcelable testPac;

    @Autowired
    public TestObj testObj;

    @Autowired
    public List<TestObj> testObjList;

    @Autowired
    public Map<String, List<TestObj>> testMap;

    private TextView mContent;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_3);
        mContent = findViewById(R.id.tv_content);

        String params = String.format(
                "name=%s,\n age=%s,\n testPac=%s,\n testObj=%s, \n testObjList=%s, \n testMap=%s",
                name,
                age,
                testPac,
                testObj,
                testObjList,
                testMap
        );

        String extra = getIntent().getStringExtra("extra");
        if (testPac != null) {
            mContent.setText(testPac.name + "---" + testPac.sex + "----extra---" + extra);
        }

    }
}

注意:需要注入的字段的命名需要跟之前添加数据时的 key 保持一至,也可以使用 Autowiredname 方法来给字段定义别名,这时候只需要保证 key 跟别名一至即可,其次,需要注入的字段不能是 private 的。

Fragment 跳转

由于 Fragment 不需要跳转,但是可以通过 ARouter 来获取 Fragment 实例,拿到实例后再做其他操作,使用方式跟 Activity 是类似的,如下:

@Route(path = "/test/TestFragment")
public class TestFragment extends BaseFragment{

    @Autowired
    public String name ;

    public static TestFragment getInstance(Activity activity) {
       return (TestFragment) ARouter.getInstance().build("/test/TestFragment").withString("name","fanda").navigation(activity);
    }
}

注意:如果路由对象是 Fragment ,则调用路由操作后是有返回值的,我们只需要强转成需要的类型即可,返回的实例是通过反射调用无参构造生成的。但如果路由对象是 Activity ,则返回值为 null

全局降级策略

路由操作时发现目标路由不存在,在 debug 模式下,会弹出 toast 表明目标不存在,这时候,我们可以通过全局降级策略来处理,跳转到一个 H5 界面或其他界面上,为什么叫全局呢?因为所有的路由操作在发现目标不存在时,都会触发该策略,自定全局降级策略的方式也很简单,实现框架的 DegradeService 接口并添加上路由地址即可,代码如下:

@Route(path = "/service/degrade")
public class DegradeServiceImpl implements DegradeService {

    @Override
    public void onLost(Context context, Postcard postcard) {
        ARouter.getInstance().build("/test/Test1Activity").navigation();
    }

    @Override
    public void init(Context context) {
         Log.e("fanda", "DegradeServiceImpl init !");
    }
}

DegradeService 是框架预留给开发者的降级处理服务,底层会在路由失败时去构建该策略实例,如果存在该策略的实现类,则会通过反射构建实例进行初始化并处理降级逻辑。

注意: 所有的服务都是在需要用到的时候才会进行初始化,如果用不到,就一直不会初始化,即不会生成实例对象。

自定义服务

当我们在进行组件化开发时,模块之间都是通过暴露对应的接口来进行功能的调用的,在需要调用服务的功能时,通过 ARouter 的路由功能即可直接拿到服务实例,而无需直接耦合服务类,自定义服务的步骤分3步:

第一步:声明接口,其他组件通过接口来调用服务。

// 需要继承于 IProvider 接口类
public interface TestService extends IProvider {
    // 定义服务功能
    void test(String content);
}

第二步: 实现接口类,并添加上路由。

@Route(path = "/service/testService")
public class TestServiceImpl implements TestService {

    Context mContext;

    @Override
    public void test(String content) {
        Toast.makeText(mContext, "Hello : " + content, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void init(Context context) {
        mContext = context;
    }
}

第三步:拿到服务类进行功能调用,有两种方式拿到实现类,第一种是通过注入的方式,即通过注解 Autowired 来自动注入,如下:

@Autowired(name = "/service/testService")
TestService testService;

第二种方式是通过 byTypebyName 的方式拿到,如下:

TestService testService = ARouter.getInstance().navigation(TestService.class); // by type 
TestService testService = (TestService) ARouter.getInstance().build("/service/testService").navigation();//by name 

注意: 当有多个服务实现类时,必须要通过 Autowired 的 name 方法来指定实现类的路由。最好使用注入方式获取,多实现时指定 name 即可。

自定义全局拦截器

在进行路由跳转时,是可以通过拦截器来进行拦截处理的,拦截器是属于全局的,所有的跳转默认都会先经过拦截器。自定义全局拦截器需要实现框架的 IInterceptor 接口,并添加 Interceptor 注解,该注解有一个 priority 方法,用来定义拦截器的优先级,数值越小优先级越高,不能设置相同优先级,多个拦截器会按优先级顺序依次执行,拦截器内的方法都是运行在子线程的,不能直接做 UI 处理。

@Interceptor(priority = 8, name = "测试拦截器")
public class TestInterceptor implements IInterceptor {

    private Context mContext;
    private Postcard mPostcard;

    @Override
    public void process(Postcard postcard, final InterceptorCallback callback) {
        mPostcard = postcard;
        //更改路径,不会使跳转的界面改变 ,只是修改了一些参数,拦截对象,做一些操作,目标不变
        mPostcard.setUri(Uri.parse("pitaya:www.wmzx.com/test/Test1Activity"));
        //注意: onContinue 或 onInterrupt 方法至少调用一个,不然跳转会无效
        if ("/test/Test3Activity".equals(postcard.getPath())) {
            postcard.setUri(Uri.parse("pitaya:www.wmzx.com/test/Test1Activity"));
            final AlertDialog.Builder ab = new AlertDialog.Builder(MainActivity.getThis());
            ab.setCancelable(false);
            ab.setTitle("温馨提醒");
            ab.setMessage("想要跳转到Test3Activity么?(触发了TestInterceptor拦截器,拦截了本次跳转)");
            ab.setNegativeButton("继续", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callback.onContinue(mPostcard);
                }
            });
            ab.setNeutralButton("登录", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // 跳到新界面,可以不执行 onContinue 和 onInterrupt ,即之前的路由目标不会继承了
                    ARouter.getInstance().build("/test/Test1Activity").navigation();
                    // 可以调用这个方法给出回调信息
                    callback.onInterrupt(null);
                }
            });
            ab.setPositiveButton("加点料", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mPostcard.withString("extra", "我是在拦截器中附加的参数");
                    callback.onContinue(mPostcard);
                }
            });

            MainLooper.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ab.create().show();
                }
            });
        } else {
            callback.onContinue(postcard);
        }
    }

    @Override
    public void init(Context context) {
        mContext = context;
        // 在 SDK 初始化时调用该方法,仅会调用一次
        Log.e("fanda", "TestInterceptor init !"+ Thread.currentThread().getName());
    }
}

注意:拦截器因为其特殊性,会被任何一次路由所触发,拦截器会在ARouter初始化的时候异步初始化,如果第一次路由的时候拦截器还没有初始化结束,路由会等待,直到初始化完成。

路由监听回调

在进行路由跳转时,可以传入监听回调,如下:

ARouter.getInstance().build(uri).navigation(this, new NavigationCallbackImpl());

private static class NavigationCallbackImpl implements NavigationCallback {
    @Override
    public void onFound(Postcard postcard) {
        // 路由目标被发现时调用
        Log.e("fanda", "找到Test3Activity!" + Thread.currentThread().getName());
    }

    @Override
    public void onLost(Postcard postcard) {
        // 路由目标找不到时调用
        Log.e("fanda", "找不到Test3Activity!" + Thread.currentThread().getName());
    }

    @Override
    public void onArrival(Postcard postcard) {
        // 路由到达之后调用
        Log.e("fanda", "Test3Activity跳转完了!" + "----分组---" + postcard.getGroup() + Thread.currentThread().getName());
    }

    @Override
    public void onInterrupt(Postcard postcard) {
        // 路由被拦截时调用
        // 这个回调是在子线程回调的
        Log.e("fanda", "Test3Activity被拦截了!" + Thread.currentThread().getName());
    }
}

从外部跳转到内部页面

通过 URI 跳转的方式,可以在 H5 界面里面通过指定的 URI 来跳转到 APP 内的指定路由页面,而且可以自动解析 URI 中的参数,最佳的实践方法可以这样做:

第一步:声明一个 activity ,这个 activity 不需要界面,只需要注册一个 intent-filter 即可,主要是用来监听 Scheme 的,代码如下:

// 用于监听Scheme事件,之后直接把url传递给ARouter即可
public class SchemeFilterActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ARouter.getInstance().build(getIntent().getData()).navigation();
        finish();
    }
}

<activity android:name=".testactivity.SchemeFilterActivity"  android:exported="true">
        <intent-filter>
            <data android:host="www.fanda.com" android:scheme="fanda"/>

            <action android:name="android.intent.action.VIEW"/>

            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
        </intent-filter>

        <!-- App Links -->
        <intent-filter android:autoVerify="true">
            <action android:name="android.intent.action.VIEW"/>

            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>

            <data
                android:host="www.fanda.com"
                android:scheme="http"/>
            <data
                android:host="www.fanda.com"
                android:scheme="https"/>
        </intent-filter>

    </activity>

SchemeFilterActivity 是对外暴露的一个中转 Activity ,所有的外部路由请求都会经过这唯一的门,然后在这个 activity 中获取到 URL 并将其交给 ARouter 来分发。比如跳转的 URIfanda://www.fanda.com/test/Test2Activity?url=www.fanda.com ,那么最终会跳转到 Test2Activity 界面并且把 url 字段赋值成 www.fanda.com 。

@Route(path = "/test/Test2Activity")
public class Test2Activity extends BaseActivity {

    @Autowired
    String url ;

}

ARouter 会自动解析 URI 中的参数并放进 IntentBundle 中,然后对 Autowired 注入的字段进行赋值操作。

注意:如果不需要自动注入功能,可能不写 ARouter.getInstance().inject(this) ,但是需要在对应的字段上添加 Autowired 注解,不然 ARouter 不会解析 URI 中的参数,即在目标页面的 Intent 中没有对应的字段。

其他一些 API 使用

// 使用绿色通道(跳过所有的拦截器)
ARouter.getInstance().build("/test/Test2Activity").greenChannel().navigation();

// 指定分组的路由请求,如果界面用 group 指定了分组,需要用这种方式路由
ARouter.getInstance().build("/test/Test2Activity","test").navigation(this);

// 指定 Flag
ARouter.getInstance().build("/test/Test2Activity","test").withFlags(Intent.FLAG_ACTIVITY_NEW_TASK).navigation(this);

为目标页面声明更多信息

上面我们讲到在目标页面至少需要配置 Route 注解的 path 方法,其实还可以自定义分组和添加一些额外的数据,代码如下:

@Route(path = "/test/Test3Activity", group = "test", extras = 100)
public class Test3Activity extends BaseActivity {

}

默认的分组为 path 定义的路由地址的第一级,可以通过 group 方法来自定义分组,同时可以通过 extras 方法来添加一个 int 数值,在拦截器里面可以通过该值来判断该页面是否需要登录或权限验证等操作。

为什么要做分组处理呢?

APP 有一百或者几百个页面的时候,一次性将所有页面都加载到内存中本身对于内存的损耗是非常可怕的,同时对于性能的损耗也是不可忽视的。所以 ARouter 中提出了分组的概念,ARouter 允许某一个模块下有多个分组,所有的分组最终会被一个 root 节点管理,每个 root 结点都会管理整个模块中的 group 节点,每个 group 结点则包含了该分组下的所有页面,ARouter 在初始化的时候只会一次性地加载所有的 root 结点,而不会加载任何一个 Group 结点,这样就会极大地降低初始化时加载结点的数量。当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是 ARouter 的按需加载。其实在整个 APP 运行的周期中,并不是所有的页面都需要被访问到,可能只有 20% 的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。